From: Christian Korber Date: Wed, 10 Sep 2025 06:30:22 +0000 (+0200) Subject: luci-mod-status,luci-mod-network: support oui to vendor resolving X-Git-Url: http://git.openwrt.org/%22https:/collectd.org//%22http:/www.crowdsec.net/%22/%22https:/collectd.org/%22http:/www.crowdsec.net/%22?a=commitdiff_plain;h=70b7176fc2547e3de1fa3139d0e25adfd3fcd66e;p=project%2Fluci.git luci-mod-status,luci-mod-network: support oui to vendor resolving For easier definition of connected devices, this commit adds support to identify them by vendor name. Package `upf-neigh` is needed to lookup the vendor name in a hash table. It implements a ubus-call `ubus call fingerprint fingerprint`: ``` root@ ~ # ubus call fingerprint fingerprint { "7c:c2:55:XX:XX:XX": { "vendor": "Super Micro Computer, Inc." } } ``` Fixes: #2065 Depends on: openwrt/packages#27257 Signed-off-by: Christian Korber --- diff --git a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js index d20c6c111f..2e897ac009 100644 --- a/modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js +++ b/modules/luci-mod-network/htdocs/luci-static/resources/view/network/dhcp.js @@ -11,6 +11,7 @@ 'require tools.dnsrecordhandlers as drh'; var callHostHints, callDUIDHints, callDHCPLeases, CBILeaseStatus, CBILease6Status; +var checkUfpInstalled, callUfpList; callHostHints = rpc.declare({ object: 'luci-rpc', @@ -30,6 +31,18 @@ callDHCPLeases = rpc.declare({ expect: { '': {} } }); +checkUfpInstalled = rpc.declare({ + object: 'file', + method: 'stat', + params: [ 'path' ] +}); + +callUfpList = rpc.declare({ + object: 'fingerprint', + method: 'fingerprint', + expect: { '': {} } +}); + CBILeaseStatus = form.DummyValue.extend({ renderWidget: function(section_id, option_id, cfgvalue) { return E([ @@ -276,12 +289,19 @@ function validateMACAddr(pools, sid, s) { return view.extend({ load: function() { return Promise.all([ - callHostHints(), - callDUIDHints(), - getDHCPPools(), - network.getNetworks(), - uci.load('firewall') - ]); + checkUfpInstalled('/usr/sbin/ufpd') + ]).then(data => { + var promises = [ + callHostHints(), + callDUIDHints(), + getDHCPPools(), + network.getNetworks(), + data[0].type === 'file' ? callUfpList() : null, + uci.load('firewall') + ] + + return Promise.all(promises); + }); }, render: function(hosts_duids_pools) { @@ -290,6 +310,7 @@ return view.extend({ duids = hosts_duids_pools[1], pools = hosts_duids_pools[2], networks = hosts_duids_pools[3], + macdata = hosts_duids_pools[4], m, s, o, ss, so, dnss; let noi18nstrings = { @@ -1249,16 +1270,46 @@ return view.extend({ so.rmempty = true; so.cfgvalue = function(section) { var macs = uci.get('dhcp', section, 'mac'); + var formattedMacs; + var hint, entry; + if(!Array.isArray(macs)){ - return expandAndFormatMAC(L.toArray(macs)); + formattedMacs = expandAndFormatMAC(L.toArray(macs)); } else { - return expandAndFormatMAC(macs); + formattedMacs = expandAndFormatMAC(macs); + } + + if (!macdata) { + return formattedMacs; } + + + if (Array.isArray(formattedMacs)){ + for (let mac in formattedMacs) { + entry = formattedMacs[mac].toLowerCase(); + if (macdata[entry]) { + hint = macdata[entry].vendor ? macdata[entry].vendor : null; + formattedMacs[mac] += ` (${hint})`; + } + } + return formattedMacs; + } + + if (formattedMacs) { + entry = formattedMacs[0].toLowerCase(); + hint = macdata[entry].vendor ? macdata[entry].vendor : null; + formattedMacs[0] += ` (${hint})`; + } + return formattedMacs; }; //removed jows renderwidget function which hindered multi-mac entry so.validate = validateMACAddr.bind(so, pools); Object.keys(hosts).forEach(function(mac) { - var hint = hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0]; + var vendor; + var lower_mac = mac.toLowerCase(); + if (macdata) + vendor = macdata[lower_mac] ? macdata[lower_mac].vendor : null; + const hint = vendor || hosts[mac].name || L.toArray(hosts[mac].ipaddrs || hosts[mac].ipv4)[0]; so.value(mac, hint ? '%s (%s)'.format(mac, hint) : mac); }); @@ -1365,6 +1416,7 @@ return view.extend({ cbi_update_table(mapEl.querySelector('#lease_status_table'), leases.map(function(lease) { var exp; + var vendor; if (lease.expires === false) exp = E('em', _('unlimited')); @@ -1373,6 +1425,13 @@ return view.extend({ else exp = '%t'.format(lease.expires); + for (let mac in macdata) { + if (mac.toUpperCase() === lease.macaddr) { + vendor = macdata[mac].vendor ? + ` (${macdata[mac].vendor})` : null; + } + } + var hint = lease.macaddr ? hosts[lease.macaddr] : null, name = hint ? hint.name : null, host = null; @@ -1385,7 +1444,7 @@ return view.extend({ return [ host || '-', lease.ipaddr, - lease.macaddr, + vendor ? lease.macaddr + vendor : lease.macaddr, exp ]; }), diff --git a/modules/luci-mod-network/root/usr/share/rpcd/acl.d/luci-mod-network.json b/modules/luci-mod-network/root/usr/share/rpcd/acl.d/luci-mod-network.json index 002a6d085d..42181f6ee2 100644 --- a/modules/luci-mod-network/root/usr/share/rpcd/acl.d/luci-mod-network.json +++ b/modules/luci-mod-network/root/usr/share/rpcd/acl.d/luci-mod-network.json @@ -38,7 +38,9 @@ "description": "Grant access to DHCP configuration", "read": { "ubus": { - "luci-rpc": [ "getDHCPLeases", "getDUIDHints", "getHostHints" ] + "luci-rpc": [ "getDHCPLeases", "getDUIDHints", "getHostHints" ], + "fingerprint": [ "fingerprint" ], + "file": [ "stat" ] }, "uci": [ "dhcp" ] }, diff --git a/modules/luci-mod-status/htdocs/luci-static/resources/view/status/routes.js b/modules/luci-mod-status/htdocs/luci-static/resources/view/status/routes.js index dfe19fc70a..62a3debc87 100644 --- a/modules/luci-mod-status/htdocs/luci-static/resources/view/status/routes.js +++ b/modules/luci-mod-status/htdocs/luci-static/resources/view/status/routes.js @@ -11,6 +11,18 @@ var callNetworkInterfaceDump = rpc.declare({ expect: { interface: [] } }); +var checkUfpInstalled = rpc.declare({ + object: 'file', + method: 'stat', + params: [ 'path' ] +}); + +var callUfpList = rpc.declare({ + object: 'fingerprint', + method: 'fingerprint', + expect: { '': {} } +}); + function applyMask(addr, mask, v6) { var words = v6 ? validation.parseIPv6(addr) : validation.parseIPv4(addr); var bword = v6 ? 0xffff : 0xff; @@ -32,14 +44,21 @@ function applyMask(addr, mask, v6) { return view.extend({ load: function() { return Promise.all([ - callNetworkInterfaceDump(), - L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'neigh', 'show' ]), {}), - L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'route', 'show', 'table', 'all' ]), {}), - L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'rule', 'show' ]), {}), - L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'neigh', 'show' ]), {}), - L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'route', 'show', 'table', 'all' ]), {}), - L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'rule', 'show' ]), {}) - ]); + checkUfpInstalled('/usr/sbin/ufpd') + ]).then(data => { + var promises = [ + callNetworkInterfaceDump(), + L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'neigh', 'show' ]), {}), + L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'route', 'show', 'table', 'all' ]), {}), + L.resolveDefault(fs.exec('/sbin/ip', [ '-4', 'rule', 'show' ]), {}), + L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'neigh', 'show' ]), {}), + L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'route', 'show', 'table', 'all' ]), {}), + L.resolveDefault(fs.exec('/sbin/ip', [ '-6', 'rule', 'show' ]), {}), + data[0].type === 'file' ? callUfpList() : null + ]; + + return Promise.all(promises); + }); }, getNetworkByDevice(networks, dev, addr, mask, v6) { @@ -84,7 +103,7 @@ return view.extend({ return matching_iface; }, - parseNeigh: function(s, networks, v6) { + parseNeigh: function(s, macs, networks, v6) { var lines = s.trim().split(/\n/), res = []; @@ -92,7 +111,8 @@ return view.extend({ var m = lines[i].match(/^([0-9a-f:.]+) (.+) (\S+) *$/), addr = m ? m[1] : null, flags = m ? m[2].trim().split(/\s+/) : [], - state = (m ? m[3] : null) || 'FAILED'; + state = (m ? m[3] : null) || 'FAILED', + vendor; if (!addr || state == 'FAILED' || addr.match(/^fe[89a-f][0-9a-f]:/)) continue; @@ -102,12 +122,17 @@ return view.extend({ if (!flags.lladdr) continue; + + for (let mac in macs) { + if (flags.lladdr === mac) + vendor = macs[mac].vendor; + } var net = this.getNetworkByDevice(networks, flags.dev, addr, v6 ? 128 : 32, v6); res.push([ addr, - flags.lladdr.toUpperCase(), + vendor ? flags.lladdr.toUpperCase() + ` (${vendor})` : flags.lladdr.toUpperCase(), E('span', { 'class': 'ifacebadge' }, [ net ? net : '(%s)'.format(flags.dev) ]) ]); } @@ -115,7 +140,7 @@ return view.extend({ return res; }, - parseRoute: function(s, networks, v6) { + parseRoute: function(s, macs, networks, v6) { var lines = s.trim().split(/\n/), res = []; @@ -173,7 +198,8 @@ return view.extend({ ip4rule = data[3].stdout || '', ip6neigh = data[4].stdout || '', ip6route = data[5].stdout || '', - ip6rule = data[6].stdout || ''; + ip6rule = data[6].stdout || '', + macdata = data[7]; var device_title = _('Which is used to access this %s').format(_('Target')); var target_title = _('Network and its mask that define the size of the destination'); @@ -235,7 +261,7 @@ return view.extend({ ]) ]); - cbi_update_table(neigh4tbl, this.parseNeigh(ip4neigh, networks, false), + cbi_update_table(neigh4tbl, this.parseNeigh(ip4neigh, macdata, networks, false), E('em', _('No entries available')) ); cbi_update_table(route4tbl, this.parseRoute(ip4route, networks, false), @@ -244,7 +270,7 @@ return view.extend({ cbi_update_table(rule4tbl, this.parseRule(ip4rule, networks, false), E('em', _('No entries available')) ); - cbi_update_table(neigh6tbl, this.parseNeigh(ip6neigh, networks, true), + cbi_update_table(neigh6tbl, this.parseNeigh(ip6neigh, macdata, networks, true), E('em', _('No entries available')) ); cbi_update_table(route6tbl, this.parseRoute(ip6route, networks, true), diff --git a/modules/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json b/modules/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json index 200631e97b..8c7dbf143d 100644 --- a/modules/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json +++ b/modules/luci-mod-status/root/usr/share/rpcd/acl.d/luci-mod-status.json @@ -49,7 +49,8 @@ "/sbin/ip -[46] rule show": [ "exec" ] }, "ubus": { - "file": [ "exec" ] + "file": [ "exec", "stat" ], + "fingerprint": [ "fingerprint" ] } } },